/*

Copyright 2009 Nicola Dal Pont

Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at

http://www.apache.org/licenses/LICENSE-2.0

Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.

Version ${MAJOR_VERSION}.${MINOR_VERSION}${BETA_MARK} Build ${BUILD_NUMBER}

*/
package com.astoolkit.workflow
{
	import com.astoolkit.workflow.api.*;
	import com.astoolkit.workflow.constant.*;
	
	import flash.events.IEventDispatcher;
	import flash.utils.getQualifiedClassName;
	import flash.utils.setTimeout;
	
	import mx.binding.utils.BindingUtils;
	import mx.collections.IList;
	import mx.core.ClassFactory;
	import mx.core.IFactory;
	import mx.events.PropertyChangeEvent;
	import mx.events.PropertyChangeEventKind;
	import mx.logging.ILogger;
	import mx.logging.Log;
	import mx.rpc.Fault;

	
	[DefaultProperty("children")]
	public class Workflow
		extends AbstractWorkflow 
		implements IWorkflow, IRepeater
	{
		protected static const LOGGER : ILogger =
			Log.getLogger( getQualifiedClassName( Workflow ).replace(/:+/g, "." ) );
		
		private var _feedWasSet : Boolean;
		
		//============================ GETTERS/SETTERS =============================
		
		override public function get async() : Boolean
		{
			return true;
		}
		
		override public function get contextFactory() : IFactory
		{
			return _contextFactory;
		}
		
		override public function set contextFactory( inFactory : IFactory ) : void
		{
			if( inFactory )
				_contextFactory = inFactory;
		}
		
		[Inspectable(defaultValue="serial", enumeration="parallel,serial,none")]
		override public function set flow( inFlow : String ) : void
		{
			_flow = inFlow;
		}
		
		override public function get feed() : String
		{
			return _feed;
		}
		
		[Inspectable(defaultValue="currentData", enumeration="pipeline,currentData")]
		override public function set feed( inFeed : String ) : void
		{
			_feed = inFeed;
			_feedWasSet = true;
		}
		
		
		//============================ CONSTRUCTOR =============================
		public function Workflow()
		{
			super();
			_childrenDelegate = createDelegate();
			_feed = Feed.PIPELINE;
			BindingUtils.bindSetter( onIndexChange, this, "currentDataIndex" );
		}
		
		//============================ DELEGATE =============================
		
		override protected function createDelegate() : IWorkflowDelegate
		{
			var delegate : IWorkflowDelegate = new DefaultWorkflowDelegate();
			delegate.onAbort = onSubtaskAbort;
			delegate.onInitialize = onSubtaskInitialized;
			delegate.onPrepare = onSubtaskPrepared;
			delegate.onBegin = onSubtaskBegin;
			delegate.onComplete = onSubtaskCompleted;
			delegate.onFault = onSubtaskFault
			delegate.onProgress = onSubtaskProgress;
			delegate.onResume = onSubtaskResumed;
			delegate.onSuspend = onSubtaskSuspended;
			delegate.onWarning = onSubtaskWarning;
			return delegate;
		}
		
		/**
		 * @private
		 */
		protected function onSubtaskSuspended( inTask : IWorkflowTask ) : void
		{
			if( root.context.status != TaskStatus.SUSPENDED )
			{
				context.status = TaskStatus.SUSPENDED;
				context.dispatchEvent( new WorkflowEvent( WorkflowEvent.SUSPENDED ) );
			}
			dispatchTaskEvent( WorkflowEvent.SUSPENDED, inTask );
		}
		
		/**
		 * @private
		 */
		protected function onSubtaskResumed( inTask : IWorkflowTask ) : void
		{
			if( context.status == TaskStatus.SUSPENDED )
			{
				context.status = TaskStatus.RUNNING;
				context.dispatchEvent( new WorkflowEvent( WorkflowEvent.RESUMED ) );
			}
			dispatchTaskEvent( WorkflowEvent.RESUMED, inTask );
		}
		
		/**
		 * @private
		 */
		protected function onSubtaskBegin( inTask : IWorkflowTask ) : void
		{
			dispatchTaskEvent( WorkflowEvent.STARTED, inTask );
		}
		
		/**
		 * @private
		 */
		protected function onSubtaskProgress( inTask : IWorkflowTask ) : void
		{
			if( inTask.status == TaskStatus.SUSPENDED )
				return;
			dispatchTaskEvent( WorkflowEvent.PROGRESS, inTask );
		}
		
		
		/**
		 * @private
		 */
		protected function onSubtaskWarning( inTask : IWorkflowTask, inMessage : String, inError : Fault ) : void
		{
			dispatchTaskEvent( WorkflowEvent.WARNING, inTask, inMessage, inError);
			onSubtaskCompleted( inTask );
		}
		
		/**
		 * @private
		 */
		protected function onSubtaskPrepared( inTask : IWorkflowTask ) : void
		{
			dispatchTaskEvent( WorkflowEvent.PREPARED, inTask );
		}
		
		/**
		 * @private
		 */
		protected function onSubtaskInitialized( inTask : IWorkflowTask ) : void
		{
			dispatchTaskEvent( WorkflowEvent.INITIALIZED, inTask );
		}
		
		/**
		 * @private
		 */
		protected function onSubtaskAbort( inTask : IWorkflowTask, inMessage : String ) : void
		{
			dispatchTaskEvent( WorkflowEvent.ABORTED, inTask, inMessage );
			onSubtaskCompleted( inTask );
		}

		/**
		 * @private
		 */
		protected function onSubtaskFault( inTask : IWorkflowTask, inMessage : String, inFault : Fault ) : void
		{
			if( inTask.status != TaskStatus.ABORTED )
			{
				_context.failedTask = inTask;
				if( inTask.failurePolicy == FailurePolicy.ABORT )
				{
					LOGGER.error( 
						"Task {0} ({1}) failed. Cause:\n{2}\n{3}", 
						inTask.description, getQualifiedClassName( inTask ),
						inMessage,
						(inFault ? inFault.getStackTrace() : "") 
					);
					dispatchTaskEvent( WorkflowEvent.FAULT, inTask, inMessage, inFault );
					if( _parent == null )
					{
						cleanUp();                   
					}
					abort();
				}
				else if( inTask.failurePolicy == FailurePolicy.SUSPEND )
				{
					inTask.suspend();
					onSubtaskCompleted( inTask );
				}
				else if( inTask.failurePolicy == FailurePolicy.CONTINUE )
				{
					if( iterate == Iterate.DATA )
					{
						nextData();
						onSubtaskCompleted( inTask );
					}
					else
						complete();
				}
				else if( inTask.failurePolicy.match( /^log\-/ ) )
				{
					LOGGER[ inTask.failurePolicy.replace( /^log\-/, "" ) ]( 
						"Task " + inTask.description + " failed with message:\n" + inMessage );
					complete();
				}
				else
					onSubtaskCompleted( inTask );
			}
		}
		
		protected function onSubtaskCompleted( inTask : IWorkflowTask ) : void
		{
			if( inTask == this )
			{
				dispatchTaskEvent( WorkflowEvent.COMPLETED, inTask );
				return;
			}
			if( _status == TaskStatus.ABORTED )
			{
				complete();
				return;
			}
			_processedTasks.splice( _processedTasks.indexOf( inTask ), 1 );
			if( inTask.status != TaskStatus.ABORTED )
			{
				dispatchTaskEvent( WorkflowEvent.COMPLETED, inTask );
				//inTask.delegate = null;
				if( inTask.outlet != CLOSED_OUTLET )
				{
					if( context.failedTask != inTask )
					{
						if( inTask.outlet == PIPELINE_OUTLET ) 
							_subPipelineData = inTask.taskOutput;
						else if( inTask.outlet is String && ( inTask.outlet as String ).match( /^\$\w+$/ ) )
						{
							context.variables[ ( inTask.outlet as String).substr(1) ] = inTask.taskOutput;
							IEventDispatcher( _document ).dispatchEvent(
								new PropertyChangeEvent(
									PropertyChangeEvent.PROPERTY_CHANGE,
									false,
									false,
									PropertyChangeEventKind.UPDATE,
									"$",
									Math.random(),
									context.variables
								) );
							
						}
						else if( inTask.outlet != CLOSED_OUTLET )
						{
							_subPipelineData = inTask.outlet;
						}
					}
				}
			}
			context.failedTask = null;
			if( _flow == Flow.SERIAL && _nextTaskIndex < runtimeTasks.length )
			{
				runNextTask();
				return;
			}
			else
			{
				if( _processedTasks.length == 0 )
				{
					complete();
				}
			}
		}

		
		//============================ LIFE CYCLE =============================
		
		override public function initialize() : void
		{
			if( _status != TaskStatus.STOPPED )
				return;
			super.initialize();
			
			if( !_contextFactory )
				_contextFactory = new ClassFactory( WorkflowContext );
			if( !_context )
			{
				_context = _contextFactory.newInstance() as IWorkflowContext;
			}
			if( !_parent )
			{
				_context.init();
				//retriggering task input setter
				//to make sure $.data is set even when taskInput is set
				//before running the workflow
				taskInput = _pipelineData;
			}
			if( runtimeTasks.length > 0 )
			{
				for each( var task : IWorkflowTask in runtimeTasks )
				{						
					task.delegate = _childrenDelegate;
					task.context = _context;
					if( task.async && !async )
						throw new Error( "Synchronous tasks must have a synchronous parent" );
					task.parent = this;
					task.initialize();
				}
			}
			//processInserts();
		}
		
		override public function prepare() : void
		{
			super.prepare();
			_processedTasks = [];
			_nextTaskIndex = 0;
			_currentDataIndex = -1;
			
			var task : IWorkflowTask;
			for each( task in runtimeTasks )
			{
				task.delegate = _childrenDelegate;
				task.prepare();
			}
		}
		
		/**
		 * Execution entry point for the workflow.
		 * You don't have to call this method yourself
		 * unless the this is the root workflow.
		 * @param set this to true if you're not keeping
		 * 					an external reference to this workflow
		 * 					and you want to prevent it from being
		 *  				garbage collected
		 */
		override public function begin():void
		{
			_subPipelineData = UNDEFINED;
			if(!_delegate)
				_delegate = createDelegate();
			if( !_parent )
			{
				try
				{
					initialize();
				}
				catch( e : Error )
				{
					fail( e.getStackTrace() );
					return;
				}
			}
			if( !_parent )
				prepare();
			super.begin();
			
			try
			{
				if( _dataProvider )
					_iterate = Iterate.DATA;
				if( _iterate == Iterate.DATA )
				{
					if( !_feedWasSet )
						_feed = Feed.CURRENT_DATA;
					if( !dataProvider || dataProvider.length == 0)
					{
						if( filteredPipelineData is IList && filteredPipelineData.length > 0 )
						{
							_dataProvider = IList( filteredPipelineData );
							_clearDataProviderOnComplete = true;				
						}
					}
					
					if( dataProvider != null && dataProvider.length > 0 )
					{
						try
						{
							_currentDataIndex ++;
							currentData = dataProvider.getItemAt( _currentDataIndex );
						}
						catch( e : Error )
						{
							fail( e.getStackTrace() );
							return;
						}
					}
					else
					{
						LOGGER.info( "Workflow \"" + description + "\" completes with no data");
						complete();
						return;
					}
				}
	
				if( runtimeTasks == null || runtimeTasks.length == 0 )
				{
					LOGGER.warn( "Workflow {0} has no tasks to perform", _description );
					complete();
					return;
				}
				for each( var task : IWorkflowTask in runtimeTasks )
				{
					task.prepare();
				}
				
				if( !_retainedWorkflows.hasOwnProperty(_root) )
					_retainedWorkflows[_root] = _root;
				runNextTask();
			} 
			catch( e : Error )
			{
				fail( e.getStackTrace() );
				return;
			}
		}
		
		
		
		override protected function complete() : void
		{
			if( _iterate == Iterate.DATA )
				LOGGER.debug(
					"Task '{0}' task iteration completed with index {1}", 
					description, 
					_currentDataIndex );
			else
				LOGGER.debug(
					"Task '{0}' completed", description );

			if( _iterate == null )
			{
				super.complete();
				if( _outlet != CLOSED_OUTLET )
					_pipelineData = _subPipelineData;
				if( !_parent )
					cleanUp();
			}
			else if( _iterate == Iterate.DATA )
			{
				if( _currentDataIndex == dataProvider.length -1 )
				{
					if( _outlet != CLOSED_OUTLET )
						_pipelineData = _subPipelineData;
					if ( _clearDataProviderOnComplete )
					{
						_clearDataProviderOnComplete = false;
						_dataProvider = null;
					}
					setTimeout( _deferredComplete, 1, _thread );
					
				}
				else
				{
					nextData();
					setTimeout(runNextTask, 1);
				}
			}
			else if( _iterate == Iterate.LOOP )
			{
				nextData();
				setTimeout(runNextTask, 1);
			}
			
		}
		
		override public function cleanUp() : void
		{
			super.cleanUp();
			_dataProvider = null;
			for each( var child : IWorkflowElement in runtimeTasks )
			{
				child.cleanUp();
			}
			_processedTasks = [];
			_nextTaskIndex = 0;
			if( this == _root && _retainedWorkflows.hasOwnProperty(_root) )
			{
				delete _retainedWorkflows.hasOwnProperty[_root];
			}
		}
			
		//============================ INTERNALS =============================
		
		protected function runSubTask( inTask : IWorkflowTask, inNow : Boolean = false ) : void
		{
			if( _iterate == Iterate.DATA && _nextTaskIndex == 1 )
			{
				dispatchEvent( 
					new PropertyChangeEvent( 
						PropertyChangeEvent.PROPERTY_CHANGE,
						false,
						false,
						PropertyChangeEventKind.UPDATE,
						"currentDat aIndex",
						_currentDataIndex - 1,
						_currentDataIndex,
						this ) );
				dispatchEvent( 
					new PropertyChangeEvent( 
						PropertyChangeEvent.PROPERTY_CHANGE,
						false,
						false,
						PropertyChangeEventKind.UPDATE,
						"currentData",
						null,
						currentData,
						this ) );
			}
			if( _status == TaskStatus.SUSPENDED ) {
				_context.suspendableListeners.addResumeCallBack( 
					function() : void 
					{
						runSubTask( inTask, inNow );
					} );
				return;
			}
			if( inNow || inTask.delay == 0 )
			{
				try {
					if( inTask is IWorkflow && IWorkflow( inTask ).contextFactory == null )
						IWorkflow( inTask ).contextFactory = _contextFactory;
					inTask.begin();
				}
				catch( taskError : Error )
				{
					_childrenDelegate.onFault(
						inTask, 
						taskError.message, 
						new Fault( 
							taskError.errorID.toString(), 
							taskError.message, 
							taskError.getStackTrace() ) 
					);
					LOGGER.error( taskError.getStackTrace() );
				}
			}
			else
			{
				setTimeout( runSubTask, inTask.delay, inTask, true );
			}
		}
	
		override protected function setSubtaskPipelineData( inTask : IWorkflowTask ) : void
		{
			if( _subPipelineData == UNDEFINED )
			{
				if( _feed == Feed.PIPELINE )
				{
					_subPipelineData = filteredPipelineData;
				}
				else if( _feed == Feed.CURRENT_DATA )
				{
					_subPipelineData = currentData;
				}
			}
			
			if( inTask.taskOutput == UNDEFINED )
			{
				if( inTask.inlet is String )
				{
					try
					{
						if( Object( inTask ).hasOwnProperty( inTask.inlet ) )
						{
							if( inTask[ inTask.inlet ] is Function )
							{
								var f : Function = inTask[ inTask.inlet ] as Function; 
								f.apply( inTask, [ _subPipelineData ] );
							}
							else
								inTask[ inTask.inlet ] = _subPipelineData;
						}
					}
					catch( e : Error )
					{
						throw new Error( "Error while trying to set pipeline data to  " +
							"task's property/function '" + inTask.inlet + "'\n\n" + 
							e.getStackTrace() );
					}
				}
				else if( inTask.inlet is Object ) 
				{
					try
					{
						for( var k : String in inTask.inlet )
						{
							inTask[ k ] = inTask.filteredPipelineData[ inTask.inlet[ k ] ];
						}
					}
					catch( e : Error )
					{
						throw new Error( "Error while trying to map pipeline " +
							"data properties to task properties.\n\n" + e.getStackTrace() );
					}
				}
				
				inTask.taskInput = _subPipelineData;
			}
		}
		
		protected function runNextTask() : void
		{
			if( _nextTaskIndex == runtimeTasks.length )
			{
				if( flow == Flow.SERIAL ) 
					complete();
				return;
			}
			var task : IWorkflowTask = IWorkflowTask( runtimeTasks[ _nextTaskIndex ] );
			setSubtaskPipelineData( task );
			_context.variables.setSelf( task );
			dispatchTaskEvent( WorkflowEvent.DATA_SET, task );
			_nextTaskIndex++;
			if( task is IWorkflow && IWorkflow( task ).flow == Flow.NONE )
			{
				runNextTask();
				return;
			}
			//LOGGER.debug(
			//    "Next task '{0}' ({1})", task.title, getQualifiedClassName( task ) );
			if( task.enabled && 
				( task.emptyPipelinePolicy != EmptyPipelinePolicy.SKIP || 
					_subPipelineData != EMPTY_PIPELINE ) )
			{
				_processedTasks.push( task );
				runSubTask( task );
			} else {
				runNextTask();
				return;
			}
			if( _flow == Flow.PARALLEL )
			{
				runNextTask();
			}
		}

		/*
		Setting the utility properties processingFirst and processingLast
		*/
		/**
		 * @private
		 */
		private function onIndexChange( inIndex : int ) : void
		{
			if( !dataProvider )
			{
				processingFirst = processingLast = false;
				return;
			}
			processingFirst = inIndex == 0;
			processingLast = inIndex == dataProvider.length -1;
		}
				
		
/*		protected function processInserts() : void
		{
			var task : IWorkflowTask;
			if( _insert ) 
			{
				for each( var insertEntry : Insert in _insert )
				{
					if( insertEntry.relativeTo != null )
						insertEntry.parent = insertEntry.relativeTo.parent;
					
					var childrenTasks : Vector.<IWorkflowElement> = insertEntry.parent.children;
					var insertionPoint : int;
					if( insertEntry.relativeTo != null )
					{
						var i : int;
						for( i = 0; i < childrenTasks.length; i++ )
						{
							if( insertEntry.relativeTo == childrenTasks[ i ] )
							{
								break;
							}
						}
						insertionPoint = insertEntry.position == Insert.BEFORE ? i : i + 1;
					}
					else
					{
						// if children length == 0 we always add the task starting at 0,
						// otherwise depending on the position value we add 
						// to the beginning or end of the tasks array
						insertionPoint = childrenTasks.length > 0 ?
							(insertEntry.position == Insert.BEFORE ? 0 : childrenTasks.length ) : 0;
					}
					
					for each( task in insertEntry.tasks )
					{
						childrenTasks.splice( insertionPoint, 0, task );
						insertionPoint++;
					}
					
				}
			}
			_insert = [];
			
		}*/
	}
}